namespace SimpleWebServices.Data
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Globalization;
    using System.Net;
    using System.Text;
    using System.Web;
    using System.Xml;
    using System.Xml.XPath;
    using SimpleWebServices.Configuration;
    using SimpleWebServices.Security.Cryptography;

    public sealed class FakeRepository : IRepository
    {
        private XmlDocument _xml;

        public FakeRepository()
        {
            this._xml = new XmlDocument();
            this._xml.LoadXml("<repository />");
        }

        public IXPathNavigable Xml
        {
            get
            {
                return this._xml;
            }
        }

        public static void Clear()
        {
            ServiceLocator.Clear();
            RepositoryConfiguration.Clear();
        }

        public static FakeRepository Register()
        {
            return FakeRepository.RegisterById(null as string);
        }

        public static FakeRepository Register(string xml)
        {
            return FakeRepository.RegisterById(null as string, xml);
        }

        public static FakeRepository Register(params object[] values)
        {
            return FakeRepository.RegisterById(null as string, values);
        }

        public static FakeRepository Register<T>()
            where T : IRecord
        {
            return FakeRepository.RegisterById(typeof(T).Name);
        }

        public static FakeRepository Register<T>(string xml)
            where T : IRecord
        {
            return FakeRepository.RegisterById(typeof(T).Name, xml);
        }

        public static FakeRepository Register<T>(params object[] values)
            where T : IRecord
        {
            return FakeRepository.RegisterById(typeof(T).Name, values);
        }

        public static FakeRepository RegisterById(string id)
        {
            return FakeRepository.RegisterById(id, null as string);
        }

        public static FakeRepository RegisterById(string id, string xml)
        {
            if (null == xml || 0 == xml.Length)
            {
                xml = "<repository />";
            }

            IRepository repository = null;
            if (string.IsNullOrEmpty(id))
            {
                ServiceLocator.Register<IRepository, FakeRepository>();
                repository = ServiceLocator.Resolve<IRepository>();
            }
            else
            {
                ServiceLocator.Register<IRepository, FakeRepository>(id);
                RepositoryConfiguration.Settings().Add(id, id);
                repository = ServiceLocator.Resolve<IRepository>(id);
            }

            FakeRepository result = repository as FakeRepository;
            result.LoadXml(xml);

            return result;
        }

        public static FakeRepository RegisterById(string id, params object[] values)
        {
            StringBuilder buffer = new StringBuilder();
            buffer.Append("<repository>");

            if (null != values)
            {
                foreach (object value in values)
                {
                    string xml = null;
                    string type = null;
                    Token? token = null;
                    string urn = null;
                    DateTime? created = null;
                    DateTime? modified = null;
                    HttpStatusCode status = HttpStatusCode.OK;
                    HttpCacheability cacheability = HttpCacheability.Server;
                    string etag = null;
                    string expires = null;

                    foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(value))
                    {
                        if ("xml".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            xml = (string)descriptor.GetValue(value);
                        }
                        else if ("type".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            type = (string)descriptor.GetValue(value);
                        }
                        else if ("token".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            token = (Token)descriptor.GetValue(value);
                        }
                        else if ("urn".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            if (typeof(AbsoluteUri) == descriptor.PropertyType)
                            {
                                urn = (AbsoluteUri)descriptor.GetValue(value);
                            }
                            else
                            {
                                urn = (string)descriptor.GetValue(value);
                            }
                        }
                        else if ("created".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            created = (DateTime)descriptor.GetValue(value);
                        }
                        else if ("modified".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            modified = (DateTime)descriptor.GetValue(value);
                        }
                        else if ("status".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            status = (HttpStatusCode)descriptor.GetValue(value);
                        }
                        else if ("cacheability".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            cacheability = (HttpCacheability)descriptor.GetValue(value);
                        }
                        else if ("etag".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            etag = (string)descriptor.GetValue(value);
                        }
                        else if ("expires".Equals(descriptor.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            expires = (string)descriptor.GetValue(value);
                        }
                        else
                        {
                            throw new NotSupportedException();
                        }
                    }

                    buffer.Append("<object");
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " type='{0}'", type);
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " token='{0}'", token.HasValue ? token.Value.ToString() : (string)Token.New());
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " urn='{0}'", (null == urn) ? ("urn://token/" + token.Value) : urn);
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " created='{0}'", created.HasValue ? created.Value.ToXmlString() : DateTimeFactory.UtcNow.ToXmlString());
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " modified='{0}'", modified.HasValue ? modified.Value.ToXmlString() : DateTimeFactory.UtcNow.ToXmlString());
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " status='{0}'", XmlConvert.ToString((int)status));
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " cacheability='{0}'", XmlConvert.ToString((int)cacheability));
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " etag='{0}'", etag);
                    buffer.AppendFormat(CultureInfo.InvariantCulture, " expires='{0}'", expires);
                    buffer.Append(">");
                    buffer.Append(xml);
                    buffer.Append("</object>");
                }
            }

            buffer.Append("</repository>");

            return FakeRepository.RegisterById(id, buffer.ToString());
        }

        public static void Reset()
        {
            ServiceLocator.Reset();
            RepositoryConfiguration.Reset();
        }

        public void LoadXml(string xml)
        {
            if (null == xml)
            {
                throw new ArgumentNullException("xml");
            }

            XmlDocument document = new XmlDocument();
            document.LoadXml(xml);

            this._xml = document;
        }

        bool IRepository.Delete(AbsoluteUri urn)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            bool result = false;

            XmlNode node = this.Select(urn);
            if (null != node)
            {
                node.RemoveAll();
                result = true;
            }

            return result;
        }

        bool IRepository.Delete(Token token)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }

            bool result = false;

            XmlNode node = this.Select(token);
            if (null != node)
            {
                node.RemoveAll();
                result = true;
            }

            return result;
        }

        bool IRepository.Exists(AbsoluteUri urn)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            bool result = false;

            XmlNode node = this.Select(urn);
            if (null != node)
            {
                result = true;
            }

            return result;
        }

        bool IRepository.Exists(Token token)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }

            bool result = false;

            XmlNode node = this.Select(token);
            if (null != node)
            {
                result = true;
            }

            return result;
        }

        bool IRepository.Exists(string xpath)
        {
            if (null == xpath)
            {
                throw new ArgumentNullException("xpath");
            }
            else if (0 == xpath.Length)
            {
                throw new ArgumentOutOfRangeException("xpath");
            }
            else if (xpath.Contains("']"))
            {
                throw new FormatException("Please use double quotes in the xpath expression.");
            }

            bool result = false;

            XmlNodeList list = this._xml.SelectNodes("/repository/object/node()");
            foreach (XmlNode node in list)
            {
                if (0 != node.ParentNode.SelectNodes(xpath).Count)
                {
                    result = true;
                    break;
                }
            }

            return result;
        }

        XmlDataObject IRepository.Export(AbsoluteUri urn)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            return To(this.Select(urn));
        }

        XmlDataObject IRepository.Export(Token token)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }

            return To(this.Select(token));
        }

        void IRepository.Insert(IRecord record)
        {
            if (null == record)
            {
                throw new ArgumentNullException("record");
            }
            else if (record.Token.HasValue)
            {
                throw new RepositoryException("The record already has a token.");
            }
            else if (null == record.Urn)
            {
                throw new RepositoryException("An record URN is required.");
            }
            else if (null != this._xml.SelectSingleNode(FormatXPath(record.Urn)))
            {
                throw new RepositoryException("An record with the same urn already exists.");
            }
            else if (record.Token.HasValue && null != this._xml.SelectSingleNode(FormatXPath(record.Token.Value)))
            {
                throw new RepositoryException("An record with the same token already exists.");
            }

            Token token = Token.New();
            string created = DateTimeFactory.UtcNow.ToXmlString();
            XmlNode node = this._xml.CreateElement("object");

            XmlAttribute attribute = this._xml.CreateAttribute("token");
            attribute.Value = token;
            node.Attributes.Append(attribute);

            record.Urn = new AbsoluteUri(record.Urn.ToString().Replace("{token}", token));

            attribute = this._xml.CreateAttribute("urn");
            attribute.Value = record.Urn;
            node.Attributes.Append(attribute);

            attribute = this._xml.CreateAttribute("created");
            attribute.Value = created;
            node.Attributes.Append(attribute);

            attribute = this._xml.CreateAttribute("modified");
            attribute.Value = created;
            node.Attributes.Append(attribute);

            if (record.Status.HasValue)
            {
                attribute = this._xml.CreateAttribute("status");
                attribute.Value = XmlConvert.ToString((int)record.Status.Value);
                node.Attributes.Append(attribute);
            }

            if (record.Cacheability.HasValue)
            {
                attribute = this._xml.CreateAttribute("cacheability");
                attribute.Value = XmlConvert.ToString((int)record.Cacheability.Value);
                node.Attributes.Append(attribute);
            }

            attribute = this._xml.CreateAttribute("etag");
            attribute.Value = MD5Hash.Compute(record.Entity);
            node.Attributes.Append(attribute);

            if (null != record.Expires)
            {
                attribute = this._xml.CreateAttribute("expires");
                attribute.Value = record.Expires;
                node.Attributes.Append(attribute);
            }

            attribute = this._xml.CreateAttribute("type");
            attribute.Value = string.Concat(record.GetType().FullName, ", ", record.GetType().Assembly.GetName().Name);
            node.Attributes.Append(attribute);

            node.InnerXml = record.Serialize().CreateNavigator().OuterXml;

            this._xml.DocumentElement.AppendChild(node);

            record.Token = token;
        }

        bool IRepository.Match(AbsoluteUri urn, string etag)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }
            else if (null == etag)
            {
                throw new ArgumentNullException("etag");
            }

            bool result = false;

            string xpath = FormatXPath(urn) + string.Format(CultureInfo.InvariantCulture, "[@etag='{0}']", etag);

            result = 0 != this._xml.SelectNodes(xpath).Count;

            return result;
        }

        bool IRepository.Match(Token token, string etag)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }
            else if (null == etag)
            {
                throw new ArgumentNullException("etag");
            }

            bool result = false;

            string xpath = FormatXPath(token) + string.Format(CultureInfo.InvariantCulture, "[@etag='{0}']", etag);

            result = 0 != this._xml.SelectNodes(xpath).Count;

            return result;
        }

        bool IRepository.ModifiedSince(AbsoluteUri urn, DateTime value)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            bool result = false;

            XmlNode node = this.Select(urn);
            if (null != node)
            {
                DateTime modified = XmlConvert.ToDateTime(node.Attributes["modified"].Value, XmlDateTimeSerializationMode.Utc);
                result = modified > value;
            }

            return result;
        }

        bool IRepository.ModifiedSince(Token token, DateTime value)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }

            bool result = false;

            XmlNode node = this.Select(token);
            if (null != node)
            {
                DateTime modified = XmlConvert.ToDateTime(node.Attributes["modified"].Value, XmlDateTimeSerializationMode.Utc);
                result = modified > value;
            }

            return result;
        }

        IList<T> IRepository.Query<T>(string xpath)
        {
            if (null == xpath)
            {
                throw new ArgumentNullException("xpath");
            }
            else if (0 == xpath.Length)
            {
                throw new ArgumentOutOfRangeException("xpath");
            }
            else if (xpath.Contains("']"))
            {
                throw new FormatException("Please use double quotes in the xpath expression.");
            }

            List<T> result = new List<T>();

            XmlNodeList list = this._xml.SelectNodes("/repository/object/node()");
            foreach (XmlNode node in list)
            {
                if (0 != node.ParentNode.SelectNodes(xpath).Count)
                {
                    result.Add(To<T>(node.ParentNode));
                }
            }

            return result;
        }

        T IRepository.Select<T>(AbsoluteUri urn)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            return To<T>(this.Select(urn));
        }

        T IRepository.Select<T>(Token token)
        {
            if (token.IsEmpty)
            {
                throw new ArgumentOutOfRangeException("token");
            }

            return To<T>(this.Select(token));
        }

        Token? IRepository.Token(AbsoluteUri urn)
        {
            if (null == urn)
            {
                throw new ArgumentNullException("urn");
            }

            Token? result = null;

            XmlNode node = this.Select(urn);
            if (null != node)
            {
                result = new Guid(node.Attributes["token"].Value);
            }

            return result;
        }

        bool IRepository.Update(IRecord record)
        {
            if (null == record)
            {
                throw new ArgumentNullException("record");
            }
            else if (!record.Token.HasValue)
            {
                throw new RepositoryException("An record token is required.");
            }
            else if (record.Token.Value.IsEmpty)
            {
                throw new RepositoryException("An empty record token is invalid.");
            }
            else if (null == record.Urn)
            {
                throw new RepositoryException("An record URN is required.");
            }

            XmlAttribute attribute = null;
            bool result = false;

            XmlNode node = this.Select(record.Token.Value);
            if (null != node)
            {
                node.Attributes["urn"].Value = record.Urn;

                node.Attributes["modified"].Value = DateTimeFactory.UtcNow.ToXmlString();
                if (record.Status.HasValue)
                {
                    node.Attributes["status"].Value = XmlConvert.ToString((int)record.Status.Value);
                }

                if (record.Cacheability.HasValue)
                {
                    node.Attributes["cacheability"].Value = XmlConvert.ToString((int)record.Cacheability.Value);
                }

                node.Attributes["etag"].Value = MD5Hash.Compute(record.Entity);
                if (null == record.Expires)
                {
                    attribute = node.Attributes["expires"];
                    if (null != attribute)
                    {
                        node.Attributes.Remove(attribute);
                    }
                }
                else
                {
                    if (null == node.Attributes["expires"])
                    {
                        attribute = this._xml.CreateAttribute("expires");
                        attribute.Value = record.Expires;
                        node.Attributes.Append(attribute);
                    }
                    else
                    {
                        node.Attributes["expires"].Value = record.Expires;
                    }
                }

                node.Attributes["type"].Value = string.Concat(record.GetType().FullName, ", ", record.GetType().Assembly.GetName().Name);

                node.InnerXml = record.Serialize().CreateNavigator().OuterXml;
                result = true;
            }

            return result;
        }

        void IRepository.Upsert(IRecord record)
        {
            if (null == record)
            {
                throw new ArgumentNullException("record");
            }

            if (record.Token.HasValue)
            {
                (this as IRepository).Update(record);
            }
            else
            {
                (this as IRepository).Insert(record);
            }
        }

        private static string FormatXPath(AbsoluteUri urn)
        {
            return string.Format(CultureInfo.InvariantCulture, "/repository/object[@urn='{0}']", urn);
        }

        private static string FormatXPath(Token token)
        {
            return string.Format(CultureInfo.InvariantCulture, "/repository/object[@token='{0}']", token);
        }

        private static T To<T>(XmlNode node)
            where T : IRecord
        {
            T result = default(T);

            if (null != node)
            {
                result = node.InnerXml.Deserialize<T>();
                result.Token = new Guid(node.Attributes["token"].Value);
                result.Urn = node.Attributes["urn"].Value;

                if (null != node.Attributes["created"])
                {
                    result.Created = XmlConvert.ToDateTime(node.Attributes["created"].Value, XmlDateTimeSerializationMode.Utc);
                }

                if (null != node.Attributes["modified"])
                {
                    result.Modified = XmlConvert.ToDateTime(node.Attributes["modified"].Value, XmlDateTimeSerializationMode.Utc);
                }

                if (null != node.Attributes["status"])
                {
                    result.Status = (HttpStatusCode)XmlConvert.ToInt32(node.Attributes["status"].Value);
                }

                if (null != node.Attributes["cacheability"])
                {
                    result.Cacheability = (HttpCacheability)XmlConvert.ToInt32(node.Attributes["cacheability"].Value);
                }

                if (null != node.Attributes["etag"])
                {
                    result.Etag = node.Attributes["etag"].Value;
                }

                if (null != node.Attributes["expires"])
                {
                    result.Expires = node.Attributes["expires"].Value;
                }
            }

            return result;
        }

        private static XmlDataObject To(XmlNode node)
        {
            XmlDataObject result = null;

            if (null != node)
            {
                result = new XmlDataObject();

                XmlDocument xml = new XmlDocument();
                xml.LoadXml(node.InnerXml);
                result.Xml = xml;

                result.Token = new Guid(node.Attributes["token"].Value);
                result.Urn = node.Attributes["urn"].Value;

                if (null != node.Attributes["type"])
                {
                    result.ObjectType = Type.GetType(node.Attributes["type"].Value);
                }

                if (null != node.Attributes["created"])
                {
                    result.Created = XmlConvert.ToDateTime(node.Attributes["created"].Value, XmlDateTimeSerializationMode.Utc);
                }

                if (null != node.Attributes["modified"])
                {
                    result.Modified = XmlConvert.ToDateTime(node.Attributes["modified"].Value, XmlDateTimeSerializationMode.Utc);
                }

                if (null != node.Attributes["status"])
                {
                    result.Status = (HttpStatusCode)XmlConvert.ToInt32(node.Attributes["status"].Value);
                }

                if (null != node.Attributes["cacheability"])
                {
                    result.Cacheability = (HttpCacheability)XmlConvert.ToInt32(node.Attributes["cacheability"].Value);
                }

                if (null != node.Attributes["expires"])
                {
                    result.Expires = node.Attributes["expires"].Value;
                }
            }

            return result;
        }

        private XmlNode Select(AbsoluteUri urn)
        {
            return this._xml.SelectSingleNode(FormatXPath(urn));
        }

        private XmlNode Select(Token token)
        {
            return this._xml.SelectSingleNode(FormatXPath(token));
        }
    }
}